<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>

<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <title>
    Javascript Audio Processing
  </title>
  <style type="text/css">
    .prettyprint ol.linenums>li {
      list-style-type: decimal
    }
  </style>
</head>

<body>

  <h1> Test hot-swapping between AudioWorklet processors </h1>
  <p id="funcs"></p>
  <p>
    Play and stop to hot-swap a simple sinewave with a noise function, both with gain control. [You might need to clean the cache when you are testing your worklet processor
    code]
  </p>

  <button id="playButton">Play</button>
  <button id="stopButton">Stop</button>
  <button id="plusButton">+</button>
  <button id="minusButton">–</button>

  <script type="text/javascript">
    let audioContext;
    let customNode;
    let customProcessorName = 'maxi-processor';
    let workletUrl = 'maxi-processor.js';

    let processorCount = 0;
    let il2pCode;

    class MaxiNode extends AudioWorkletNode {

      constructor(audioContext, processorName) {
        super(audioContext, processorName);
      }
    }

    document.addEventListener("DOMContentLoaded", () => {
      setButtonEventHandlers();
    });

    function setButtonEventHandlers() {

      const playButton = document.getElementById('playButton');
      playButton.addEventListener("click", () => playAudio());

      const stopButton = document.getElementById('stopButton');
      stopButton.addEventListener("click", () => stopAudio());

      const plusButton = document.getElementById('plusButton');
      plusButton.addEventListener("click", () => increaseVolume());

      const minusButton = document.getElementById('minusButton');
      minusButton.addEventListener("click", () => decreaseVolume());
    }

    /**
     * @createAndRegisterCustomProcessorCode
     */
    function createAndRegisterCustomProcessorCode(expression, processorName) {

      let userDefinedFunction = "";
      switch (expression % 2) {
        case 0:
          userDefinedFunction = `Math.random() * 2`;
          break;
        case 1:
          userDefinedFunction = `Math.sin(2 * Math.PI * this.frequency * this.time)`;
          break;
        default:
          userDefinedFunction = `Math.sin(2 * Math.PI * this.frequency * this.time)`;
      }

      // We get an "Error on loading worklet:  DOMException" with the following import:
      // import Module from './maximilian.wasmmodule.js';
      // import Module from "${new URL("./maximilian.wasmmodule.js", location.href)}";
      return `
       
        class CustomProcessor extends AudioWorkletProcessor {
          static get parameterDescriptors() {
            return [{
              name: 'gain',
              defaultValue: 0.1
            }];
          }
          constructor() {
            super();
            this.time = 0;
            this.frequency = 440;
            this.sampleRate = 44100;

            this.port.onmessage = (event) => {
              console.log(event.data);
            };

          }
          process(inputs, outputs, parameters) {

            const outputsLength = outputs.length;
            for (let outputId = 0; outputId < outputsLength; ++outputId) {
              let output = outputs[outputId];
              const channelLenght = output.length;

              for (let channelId = 0; channelId < channelLenght; ++channelId) {
                const gain = parameters.gain;
                const isConstant = gain.length === 1
                let outputChannel = output[channelId];
                for (let i = 0; i < outputChannel.length; ++i) {
                  const gain = parameters.gain.length === 1? parameters.gain[0] : parameters.gain[i];
                  outputChannel[i] = ${userDefinedFunction} * gain;
                  this.time += 1/this.sampleRate;
                }
              }
            }
            return true;
          }
        }

        registerProcessor("${processorName}", CustomProcessor);`;
    }

    /**
     * TODO: Check for memory leaks
     * @runProcessorCode
     */
    function playAudio() {

      if (audioContext === undefined)
        audioContext = new AudioContext();

      console.log('processorCount: ' + processorCount);
      // const userCode = editor.getDoc().getValue();
      const processorName = `processor-${processorCount}`;

      const code = createAndRegisterCustomProcessorCode(processorCount, processorName);

      console.log(code);

      const blob = new Blob([code], {
        type: "application/javascript; charset=utf-8"
      });

      // TODO: Check for memory leaks
      // URL.revokeObjectURL()
      const workletUrl = window.URL.createObjectURL(blob);

      // Set custom processor in audio worklet
      audioContext.audioWorklet.addModule(workletUrl).then(() => {
        stopAudio();

        customNode = new MaxiNode(audioContext, processorName);

        customNode.onprocessorerror = (event) => {
          console.log(`An error from AudioWorkletProcessor.constructor() was detected.`);
        };

        customNode.port.onmessage = (event) => {
          //  data from the processor.
          console.log("from processor: " + event.data);
        };
        customNode.connect(audioContext.destination);
      }).catch(e => console.log("Error on loading worklet: ", e));
    
      processorCount++;
    
    }



    function playFileWorklet() {

      if (audioContext === undefined)
        audioContext = new AudioContext();

      try {
        audioContext.audioWorklet.addModule(workletUrl).then(() => {
          stopAudio();
          customNode = new MaxiNode(audioContext, customProcessorName);

          customNode.onprocessorerror = (event) => {
            console.log(`An error from AudioWorkletProcessor.constructor() was detected.`);
          };

          customNode.port.onmessage = (event) => {
            //  data from the processor.
            console.log("from processor: " + event.data);
          };

          customNode.connect(audioContext.destination);
        }).catch((e => console.log("Error on loading worklet: ", e)));
      } catch (err) {
        console.log("AudioWorklet not supported in this browser: ", err.message);
      }
    }

    function stopAudio() {
      if (customNode !== undefined) {
        customNode.disconnect(audioContext.destination);
        customNode = undefined;
      }
    }

    function increaseVolume() {
      if (customNode !== undefined) {
        const gainParam = customNode.parameters.get('gain');
        gainParam.value += 0.1;
      }
    }

    function decreaseVolume() {
      if (customNode !== undefined) {
        const gainParam = customNode.parameters.get('gain');
        gainParam.value -= 0.1;
      }
    }
  </script>
</body>

</html>
